Проект: Исследование продаж продуктов питания через мобильное приложение¶

Цели исследования:

  1. Определить конверсию в покупку

    Задачи:

    • изучить воронку продаж
    • сколько пользователей остается на предыдущих этапах, не доходя до покупки
    • на каких этапах "застревают" клиенты
  2. Определить эффективно ли изменение шрифта в приложении

    Задачи:

    • провести А/А/В-тест

Так как качество данных неизвестно, то необходимо также провести предобработку данных.

Ход исследования:

  1. Изучение общей информации в файле с данными
  2. Подготовка данных к анализу
  3. Проверка данных
  4. Изучение воронки событий
  5. Изучение результатов эксперимента
  6. Написание выводов и рекомендаций

Общая информация о данных¶

In [1]:
#Импорт необходимых библиотек
import pandas as pd
import datetime as dt
from datetime import datetime
import numpy as np
import matplotlib.pyplot as plt
#from pandas.plotting import register_matplotlib_converters
import warnings
warnings.filterwarnings('ignore')
import seaborn as sns
import scipy.stats as stats
from scipy import stats as st
import math as mth
In [2]:
#чтение файла
try:
    data = pd.read_csv('...', sep='\t')
except FileNotFoundError:
    print('Укажи верный путь к файлу')
    
#вывод первых строк 
data.head(10)
Out[2]:
EventName DeviceIDHash EventTimestamp ExpId
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248
5 CartScreenAppear 6217807653094995999 1564055323 248
6 OffersScreenAppear 8351860793733343758 1564066242 246
7 MainScreenAppear 5682100281902512875 1564085677 246
8 MainScreenAppear 1850981295691852772 1564086702 247
9 MainScreenAppear 5407636962369102641 1564112112 246
In [3]:
#основная информация о данных
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 4 columns):
 #   Column          Non-Null Count   Dtype 
---  ------          --------------   ----- 
 0   EventName       244126 non-null  object
 1   DeviceIDHash    244126 non-null  int64 
 2   EventTimestamp  244126 non-null  int64 
 3   ExpId           244126 non-null  int64 
dtypes: int64(3), object(1)
memory usage: 7.5+ MB
In [4]:
# проверка на явные дубликаты
data.duplicated().sum()
Out[4]:
413
In [5]:
pers = data.duplicated().sum()/data['EventName'].count()
f'Процент дубликатов в датасета: {pers:.3%}'
Out[5]:
'Процент дубликатов в датасета: 0.169%'

Вывод по шагу 1:

  1. Следует переименовть названия столбцов, чтобы привести наименования к "змеиному" регистру
  2. В датасете отсутствуют пропуски
  3. В датасете обнаружено 413 явных дубликата, пользователь вряд ли может совершить одно и то же действие в одно и то же время несколько раз, поэтому лучше удалить дубликаты на следующем шаге, тем более, что процент дубликатов менее 1%.

Подготовка данных к анализу¶

Очистка датасета от явных дубликатов¶

In [6]:
data = data.drop_duplicates()
data = data.reset_index( drop = True )
data
Out[6]:
EventName DeviceIDHash EventTimestamp ExpId
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248
... ... ... ... ...
243708 MainScreenAppear 4599628364049201812 1565212345 247
243709 MainScreenAppear 5849806612437486590 1565212439 246
243710 MainScreenAppear 5746969938801999050 1565212483 246
243711 MainScreenAppear 5746969938801999050 1565212498 246
243712 OffersScreenAppear 5746969938801999050 1565212517 246

243713 rows × 4 columns

Переименование столбцов¶

In [7]:
data.columns = ['event', 'device_id', 'event_timestamp', 'exp_id']
data.head(1)
Out[7]:
event device_id event_timestamp exp_id
0 MainScreenAppear 4575588528974610257 1564029816 246

Пропуски и типы данных¶

Пропуски в датасете отсутствуют.

Все типы данные соответствуют отображаемым данным за исключением даты, которую исправим на следующем шаге. Дату необходимо привести к типу datetime, а также имеет смысл выделить отдельный столбец с датой без времени.

Работа с датами¶

In [8]:
# создаем столбец с датой и временем
data['event_time'] = pd.to_datetime(data['event_timestamp'], unit = 's')
#создаем столбец с датой
data['event_date'] = pd.to_datetime(data['event_time']).dt.date
#исключаем столбец с исходной датой в секундах
data = data.drop(columns = ['event_timestamp'],axis = 1)
data
Out[8]:
event device_id exp_id event_time event_date
0 MainScreenAppear 4575588528974610257 246 2019-07-25 04:43:36 2019-07-25
1 MainScreenAppear 7416695313311560658 246 2019-07-25 11:11:42 2019-07-25
2 PaymentScreenSuccessful 3518123091307005509 248 2019-07-25 11:28:47 2019-07-25
3 CartScreenAppear 3518123091307005509 248 2019-07-25 11:28:47 2019-07-25
4 PaymentScreenSuccessful 6217807653094995999 248 2019-07-25 11:48:42 2019-07-25
... ... ... ... ... ...
243708 MainScreenAppear 4599628364049201812 247 2019-08-07 21:12:25 2019-08-07
243709 MainScreenAppear 5849806612437486590 246 2019-08-07 21:13:59 2019-08-07
243710 MainScreenAppear 5746969938801999050 246 2019-08-07 21:14:43 2019-08-07
243711 MainScreenAppear 5746969938801999050 246 2019-08-07 21:14:58 2019-08-07
243712 OffersScreenAppear 5746969938801999050 246 2019-08-07 21:15:17 2019-08-07

243713 rows × 5 columns

Вывод по шагу 2:

  1. Удалены явные дубликаты
  2. Наименования столбцов приведены к удобному виду
  3. Даты событий приведены в удобный формат, как для чтения, так и для анализа
  4. Добавлен столбец только с датой
  5. Данные подготовлены для дальнейшего анализа

Изучение и проверка данных¶

Количество событий и пользователей в логе¶

In [9]:
#Расчет общего числа событий
event_nmb = data['event'].count()
f'Кoличество событий равно количеству записей в датасете и равно: {event_nmb}'
Out[9]:
'Кoличество событий равно количеству записей в датасете и равно: 243713'
In [10]:
#Расчет количества пользователей
users_nmb = data['device_id'].nunique()
f'Кoличество уникальных пользователей в логе: {users_nmb}'
Out[10]:
'Кoличество уникальных пользователей в логе: 7551'
In [11]:
# расчет среднего
events_per_user = event_nmb/users_nmb
f'В среднем на одного пользователя приходится {events_per_user:.0f} события'
Out[11]:
'В среднем на одного пользователя приходится 32 события'

Вывод: на одного пользователя в среднем приходится 32 события. Всего в данных 7551 пользователь и совершилось 273 713 событий.

Изучение временного периода¶

In [12]:
#вывод минимальной даты
mindate = data['event_date'].min()
print(mindate)
2019-07-25
In [13]:
#вывод максимальной даты
maxdate = data['event_date'].max()
print(maxdate)
2019-08-07
In [14]:
# построекние гистограмы числа событий по дате и времени
data['event_time'].hist(bins=120, figsize = (15,5))
plt.title('Распределение экспериментов по времени');
In [15]:
#очищаем данные, убрав более старые данные 
data_cleaned = data[data['event_time'] >= '2019-08-01']
data_cleaned.head()
Out[15]:
event device_id exp_id event_time event_date
2826 Tutorial 3737462046622621720 246 2019-08-01 00:07:28 2019-08-01
2827 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:00 2019-08-01
2828 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:55 2019-08-01
2829 OffersScreenAppear 3737462046622621720 246 2019-08-01 00:08:58 2019-08-01
2830 MainScreenAppear 1433840883824088890 247 2019-08-01 00:08:59 2019-08-01
In [16]:
#проверяем наличие всех груп и их сбаллансированность
users_A1 = data_cleaned[data_cleaned['exp_id']==246]['device_id'].nunique()
users_A2 = data_cleaned[data_cleaned['exp_id']==247]['device_id'].nunique()
users_B = data_cleaned[data_cleaned['exp_id']==248]['device_id'].nunique()

print(f'Количество пользователей в группе А (246):{users_A1}')
print(f'Количество пользователей в группе А (247):{users_A2}')
print(f'Количество пользователей в группе B (248):{users_B}')
Количество пользователей в группе А (246):2484
Количество пользователей в группе А (247):2513
Количество пользователей в группе B (248):2537
In [17]:
#проверим нет ли пересекающихся пользователей
data_cleaned.groupby('device_id').agg({'exp_id':'nunique'}).query('exp_id > 1').count()
Out[17]:
exp_id    0
dtype: int64
In [18]:
# проверим какой процент событий мы потеряли при отсечении дат
data_events = data.groupby('event').agg({'event':'count'}).rename(columns = {'event':'events_amt'}).sort_values(by='events_amt', ascending = False).reset_index()
data_events_cleaned = data_cleaned.groupby('event').agg({'event':'count'}).rename(columns = {'event':'events_amt_cleaned'}).sort_values(by='events_amt_cleaned', ascending = False).reset_index()
data_events = data_events.merge(data_events_cleaned, on ='event')
#добавим строку с суммой
total = {'event':'total', 'events_amt': data_events['events_amt'].sum(), 'events_amt_cleaned':data_events['events_amt_cleaned'].sum()}
data_events = data_events.append(total, ignore_index = True)
#добавим столбец с процентом отклонения
data_events['percentage_lost,%'] = round((1 - data_events['events_amt_cleaned']/data_events['events_amt'])*100, 2)
data_events
Out[18]:
event events_amt events_amt_cleaned percentage_lost,%
0 MainScreenAppear 119101 117328 1.49
1 OffersScreenAppear 46808 46333 1.01
2 CartScreenAppear 42668 42303 0.86
3 PaymentScreenSuccessful 34118 33918 0.59
4 Tutorial 1018 1005 1.28
5 total 243713 240887 1.16
In [19]:
# проверим какой процент пользователей мы потеряли при отсечении дат
data_users = data.groupby('exp_id').agg({'device_id':'nunique'}).rename(columns = {'device_id':'users_amt'}).sort_values(by='exp_id').reset_index()
data_users_cleaned = data_cleaned.groupby('exp_id').agg({'device_id':'nunique'}).rename(columns = {'device_id':'users_amt_cleaned'}).sort_values(by='exp_id').reset_index()
data_users = data_users.merge(data_users_cleaned, on ='exp_id')
#добавим строку с суммой
total = {'exp_id':'total', 'users_amt': data_users['users_amt'].sum(), 'users_amt_cleaned':data_users['users_amt_cleaned'].sum()}
data_users = data_users.append(total, ignore_index = True)
#добавим столбец с процентом отклонения
data_users['percentage_lost,%'] = round((1 - data_users['users_amt_cleaned']/data_users['users_amt'])*100, 2)
data_users
Out[19]:
exp_id users_amt users_amt_cleaned percentage_lost,%
0 246 2489 2484 0.20
1 247 2520 2513 0.28
2 248 2542 2537 0.20
3 total 7551 7534 0.23

Выводы по шагу 3:

  1. Обнаружено, что данные не за весь период достаточного объема. В связи с этим принято решение отбросить малоинформативные данные. Теперь мы обладаем данными за период с 1 августа 2019 года по 7 августа 2019 года (отбросив половину дат)
  2. Проверив распределение пользователей по группам, видим, что группы сбалансированы и них отсутствую пересечения, значит эксперимент был проведен качественно.
  3. Проверив распределение количества событий и пользователей по группам до и после очистки данных по датам, видим, что мы потеряли в общей сложности 1.16% событий (не более 1.49% одного события) и 0.28 % уникальных пользователей (не более 0.32% для группы эксперимента). Данные значения достаточно малы, чтобы исключенными событиями и пользователями можно было пренебречь в анализе.

Исследование воронки событий¶

Исследование событий по частоте и пользователям¶

In [20]:
# расчет сколько раз совершалось каждое событие
events_amt = data_cleaned.groupby('event').agg({'event':'count'}).rename(columns = {'event':'events_amt'}).sort_values(by='events_amt', ascending = False)
events_amt
Out[20]:
events_amt
event
MainScreenAppear 117328
OffersScreenAppear 46333
CartScreenAppear 42303
PaymentScreenSuccessful 33918
Tutorial 1005
In [21]:
# расчет числа пользователей, которые совершили каждое событие
users_event_amt = data_cleaned.groupby('event').agg({'device_id':'nunique'}).rename(columns = {'device_id':'users_amt'}).sort_values(by='users_amt', ascending = False)
users_event_amt
Out[21]:
users_amt
event
MainScreenAppear 7419
OffersScreenAppear 4593
CartScreenAppear 3734
PaymentScreenSuccessful 3539
Tutorial 840
In [22]:
#добавим к таблице % от общего числа пользователей
users_event_amt['percentage_of_total_users'] = round(users_event_amt['users_amt']/data_cleaned['device_id'].nunique()*100,2)
users_event_amt.reset_index(inplace=True)
users_event_amt
Out[22]:
event users_amt percentage_of_total_users
0 MainScreenAppear 7419 98.47
1 OffersScreenAppear 4593 60.96
2 CartScreenAppear 3734 49.56
3 PaymentScreenSuccessful 3539 46.97
4 Tutorial 840 11.15

Предположение о последовательности событий:

  1. События MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful выстраиваются в приведенную последовательную цепочку.
  2. Однако, событие MainScreenAppear может случиться после любого события, если пользователь передумал продолжать или решил выбрать другой товар.
  3. Также событие Tutorial может быть открыто (в зависимости от функционала приложения) в любой момент. При этом это событие скорее всего не обязательно и многие пользователи его просто игнорируют.

Для дальнейшего исследования воронки исключим из нее событие Tutorial.

Конверсия пользователей¶

In [23]:
#удаление строки 
users_event_amt_cleaned = users_event_amt[users_event_amt['event'] != 'Tutorial']
users_event_amt_cleaned
Out[23]:
event users_amt percentage_of_total_users
0 MainScreenAppear 7419 98.47
1 OffersScreenAppear 4593 60.96
2 CartScreenAppear 3734 49.56
3 PaymentScreenSuccessful 3539 46.97
In [24]:
#для расчета процента перехода пользователей добавим столбец со смещенными users_amt
users_event_amt_cleaned['users_amt_shift'] = users_event_amt_cleaned['users_amt'].shift(1, fill_value=0)
#расчет прохождения на следующий этап воронки
users_event_amt_cleaned['convertion'] = round(users_event_amt_cleaned['users_amt']/users_event_amt_cleaned['users_amt_shift']*100,2)
#удалим дополнительный столбец
users_event_amt_cleaned = users_event_amt_cleaned.drop(columns = ['users_amt_shift'],axis = 1)
#добавим столбец с долей пользователей относительно 1ого шага
users_event_amt_cleaned['percentage_of_1st_event'] = (
    round(users_event_amt_cleaned['users_amt']/
          users_event_amt_cleaned['users_amt'].values [0]*100,2)
)
users_event_amt_cleaned
Out[24]:
event users_amt percentage_of_total_users convertion percentage_of_1st_event
0 MainScreenAppear 7419 98.47 inf 100.00
1 OffersScreenAppear 4593 60.96 61.91 61.91
2 CartScreenAppear 3734 49.56 81.30 50.33
3 PaymentScreenSuccessful 3539 46.97 94.78 47.70
In [25]:
#построим воронку конверсии пользователей 
from plotly import graph_objects as go

fig = go.Figure(go.Funnel(
    y = users_event_amt_cleaned['event'],
    x = users_event_amt_cleaned['users_amt'],
    textposition = "inside",
    textinfo = "value+percent initial",
    marker = {"color": ['powderblue','lightsteelblue', 'mediumslateblue','midnightblue']}
    )
)
fig.update_layout(title_text='Воронка событий (с учетом процента перехода пользователей на следующий шаг)')
fig.show()

Выводы по шагу 4:

  1. По результатам исследования частоты событий и процента пользователей, совершающих каждое событие, можно предположить, что типичной последовательностью событий является: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful
  2. Больше всего пользователей теряется на 1ом шаге при переходе с главного экрана на страницу с предложениями: остается 61.91% пользователей.
  3. До оплаты доходит 47.70% пользователей из тех, кто зашел на главную страницу.
  4. Приведенные значения наглядно отображены на воронке событий.

Анализ результатов эксперимента¶

Подготовка датасетов¶

In [26]:
# напомним количество пользователей в каждой группе с учетом очистки событи Tutorial
data_cleaned = data_cleaned[data_cleaned['event'] != 'Tutorial']

users_A1 = data_cleaned[data_cleaned['exp_id']==246]['device_id'].nunique()
users_A2 = data_cleaned[data_cleaned['exp_id']==247]['device_id'].nunique()
users_B = data_cleaned[data_cleaned['exp_id']==248]['device_id'].nunique()

print(f'Количество пользователей в 1ой группе А (246):{users_A1}')
print(f'Количество пользователей во 2ой  группе А (247):{users_A2}')
print(f'Количество пользователей в группе B (248):{users_B}')
Количество пользователей в 1ой группе А (246):2483
Количество пользователей во 2ой  группе А (247):2512
Количество пользователей в группе B (248):2535
In [27]:
# Создадим датасеты по группам
dataA1 = data_cleaned[data_cleaned['exp_id']==246]
dataA2 = data_cleaned[data_cleaned['exp_id']==247]
dataB = data_cleaned[data_cleaned['exp_id']==248]
# обобщенная группа А
dataA = data_cleaned[data_cleaned['exp_id']!=248]

display(dataA1.head())
display(dataA2.head())
display(dataB.head())
dataA.head()
event device_id exp_id event_time event_date
2827 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:00 2019-08-01
2828 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:55 2019-08-01
2829 OffersScreenAppear 3737462046622621720 246 2019-08-01 00:08:58 2019-08-01
2832 OffersScreenAppear 3737462046622621720 246 2019-08-01 00:10:26 2019-08-01
2833 MainScreenAppear 3737462046622621720 246 2019-08-01 00:10:47 2019-08-01
event device_id exp_id event_time event_date
2830 MainScreenAppear 1433840883824088890 247 2019-08-01 00:08:59 2019-08-01
2831 MainScreenAppear 4899590676214355127 247 2019-08-01 00:10:15 2019-08-01
2836 MainScreenAppear 4899590676214355127 247 2019-08-01 00:11:28 2019-08-01
2837 OffersScreenAppear 4899590676214355127 247 2019-08-01 00:11:30 2019-08-01
2841 OffersScreenAppear 4899590676214355127 247 2019-08-01 00:12:36 2019-08-01
event device_id exp_id event_time event_date
2842 MainScreenAppear 4613461174774205834 248 2019-08-01 00:14:31 2019-08-01
2843 MainScreenAppear 4613461174774205834 248 2019-08-01 00:14:34 2019-08-01
2844 CartScreenAppear 4613461174774205834 248 2019-08-01 00:14:34 2019-08-01
2845 PaymentScreenSuccessful 4613461174774205834 248 2019-08-01 00:14:43 2019-08-01
2846 OffersScreenAppear 4613461174774205834 248 2019-08-01 00:14:51 2019-08-01
Out[27]:
event device_id exp_id event_time event_date
2827 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:00 2019-08-01
2828 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:55 2019-08-01
2829 OffersScreenAppear 3737462046622621720 246 2019-08-01 00:08:58 2019-08-01
2830 MainScreenAppear 1433840883824088890 247 2019-08-01 00:08:59 2019-08-01
2831 MainScreenAppear 4899590676214355127 247 2019-08-01 00:10:15 2019-08-01
In [28]:
# создадим датасеты по группам и событиям 
def data_cleaned_by_event(data):
    data_MSA = data[data['event']=='MainScreenAppear']
    data_OSA = data[data['event']=='OffersScreenAppear']
    data_CSA = data[data['event']=='CartScreenAppear']
    data_PSS = data[data['event']=='PaymentScreenSuccessful']
    display(data_MSA.head(2))
    display(data_OSA.head(2))
    display(data_CSA.head(2))
    display(data_PSS.head(2))
    return data_MSA, data_OSA, data_CSA, data_PSS

dataA1MSA, dataA1OSA, dataA1CSA, dataA1PSS  = data_cleaned_by_event(dataA1)
dataA2MSA, dataA2OSA, dataA2CSA, dataA2PSS  = data_cleaned_by_event(dataA2)
dataBMSA, dataBOSA, dataBCSA, dataBPSS  = data_cleaned_by_event(dataB)
dataAMSA, dataAOSA, dataACSA, dataAPSS  = data_cleaned_by_event(dataA)
event device_id exp_id event_time event_date
2827 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:00 2019-08-01
2828 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:55 2019-08-01
event device_id exp_id event_time event_date
2829 OffersScreenAppear 3737462046622621720 246 2019-08-01 00:08:58 2019-08-01
2832 OffersScreenAppear 3737462046622621720 246 2019-08-01 00:10:26 2019-08-01
event device_id exp_id event_time event_date
2991 CartScreenAppear 5653442602434498252 246 2019-08-01 00:52:53 2019-08-01
2998 CartScreenAppear 5653442602434498252 246 2019-08-01 00:56:16 2019-08-01
event device_id exp_id event_time event_date
2990 PaymentScreenSuccessful 5653442602434498252 246 2019-08-01 00:52:53 2019-08-01
2997 PaymentScreenSuccessful 5653442602434498252 246 2019-08-01 00:56:13 2019-08-01
event device_id exp_id event_time event_date
2830 MainScreenAppear 1433840883824088890 247 2019-08-01 00:08:59 2019-08-01
2831 MainScreenAppear 4899590676214355127 247 2019-08-01 00:10:15 2019-08-01
event device_id exp_id event_time event_date
2837 OffersScreenAppear 4899590676214355127 247 2019-08-01 00:11:30 2019-08-01
2841 OffersScreenAppear 4899590676214355127 247 2019-08-01 00:12:36 2019-08-01
event device_id exp_id event_time event_date
2862 CartScreenAppear 2712290788139738557 247 2019-08-01 00:22:45 2019-08-01
2866 CartScreenAppear 2712290788139738557 247 2019-08-01 00:23:09 2019-08-01
event device_id exp_id event_time event_date
2861 PaymentScreenSuccessful 2712290788139738557 247 2019-08-01 00:22:44 2019-08-01
2865 PaymentScreenSuccessful 2712290788139738557 247 2019-08-01 00:23:08 2019-08-01
event device_id exp_id event_time event_date
2842 MainScreenAppear 4613461174774205834 248 2019-08-01 00:14:31 2019-08-01
2843 MainScreenAppear 4613461174774205834 248 2019-08-01 00:14:34 2019-08-01
event device_id exp_id event_time event_date
2846 OffersScreenAppear 4613461174774205834 248 2019-08-01 00:14:51 2019-08-01
2851 OffersScreenAppear 6121366368901703338 248 2019-08-01 00:15:54 2019-08-01
event device_id exp_id event_time event_date
2844 CartScreenAppear 4613461174774205834 248 2019-08-01 00:14:34 2019-08-01
3062 CartScreenAppear 1694940645335807244 248 2019-08-01 01:13:06 2019-08-01
event device_id exp_id event_time event_date
2845 PaymentScreenSuccessful 4613461174774205834 248 2019-08-01 00:14:43 2019-08-01
3064 PaymentScreenSuccessful 1694940645335807244 248 2019-08-01 01:13:12 2019-08-01
event device_id exp_id event_time event_date
2827 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:00 2019-08-01
2828 MainScreenAppear 3737462046622621720 246 2019-08-01 00:08:55 2019-08-01
event device_id exp_id event_time event_date
2829 OffersScreenAppear 3737462046622621720 246 2019-08-01 00:08:58 2019-08-01
2832 OffersScreenAppear 3737462046622621720 246 2019-08-01 00:10:26 2019-08-01
event device_id exp_id event_time event_date
2862 CartScreenAppear 2712290788139738557 247 2019-08-01 00:22:45 2019-08-01
2866 CartScreenAppear 2712290788139738557 247 2019-08-01 00:23:09 2019-08-01
event device_id exp_id event_time event_date
2861 PaymentScreenSuccessful 2712290788139738557 247 2019-08-01 00:22:44 2019-08-01
2865 PaymentScreenSuccessful 2712290788139738557 247 2019-08-01 00:23:08 2019-08-01

Проверка статистической разницы между выборками¶

Для проверки статистической разницы между всеми группами необходимо провести в общей сложности 16 тестов:

  1. 4 для проверки разницы между группами А (для каждого события)
  2. 4 для сравнения группы В и группы А1
  3. 4 для сравнения группы В и группы А2
  4. 4 для сравнения группы В и обобщенной группы А

Для всех тестов будут приняты следующие гипотезы:

  1. Нулевая гипотеза: между группами пользователей нет значимой разницы.
  2. Альтернативная гипотеза: между группами пользователей есть значимая разница.

Подготовка функции¶

In [29]:
def statistic_check(data_total_1,data_total_2, data_success_1, data_success_2, alpha):

    customers = np.array([data_total_1['device_id'].nunique(), data_total_2['device_id'].nunique()])
    success = np.array([data_success_1['device_id'].nunique(), data_success_2['device_id'].nunique()])

    p1 = success[0]/customers[0]
    p2 = success[1]/customers[1]
    p_combined = (success[0] + success[1]) / (customers[0] + customers[1])
    difference = p1 - p2 
    z_value =  difference / mth.sqrt(p_combined * (1 - p_combined) * (1/customers[0] + 1/customers[1])) 
    distr = st.norm(0, 1)  
    p_value = (1 - distr.cdf(abs(z_value))) * 2 
    print('p-значение: ', round(p_value,2))
    if p_value < alpha: 
        print('Отвергаем нулевую гипотезу: между группами есть значимая разница')
    else:
        print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными')

Определение уровня статистической значимости¶

В данном исследовании мы проводим множественные сравнение. То есть повышается вероятность появления ошибки первого роды. Для снижения вероятности групповой ошибки 1ого рода необходимо применить поправку.

Самой распространенной является поправка Бонферрони, ее и используем в данном исследовании.

Но надо отметить, что если будет нужно повысить мощность теста, сохраняя FWER < ɑ, можно будет применить методы Холма и Шидака.

In [30]:
alpha = 0.05
alpha_bonferroni = alpha/16
alpha_bonferroni
Out[30]:
0.003125

Проведение сравнений между группами А¶

In [31]:
statistic_check(dataA1, dataA2, dataA1MSA, dataA2MSA, alpha_bonferroni)
p-значение:  0.75
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [32]:
statistic_check(dataA1, dataA2, dataA1OSA, dataA2OSA, alpha_bonferroni)
p-значение:  0.25
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [33]:
statistic_check(dataA1, dataA2, dataA1CSA, dataA2CSA, alpha_bonferroni)
p-значение:  0.23
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [34]:
statistic_check(dataA1, dataA2, dataA1PSS, dataA2PSS, alpha_bonferroni)
p-значение:  0.11
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными

Вывод:

во всех четырех группах по событиям была отвергнута нулевая гипотеза, значит у нас нет оснований, считать, что между группами А есть значимая разница.

Проведение сравнения между группами А1 и В¶

In [35]:
statistic_check(dataA1, dataB, dataA1MSA, dataBMSA, alpha_bonferroni)
p-значение:  0.34
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [36]:
statistic_check(dataA1, dataB, dataA1OSA, dataBOSA, alpha_bonferroni)
p-значение:  0.21
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [37]:
statistic_check(dataA1, dataB, dataA1CSA, dataBCSA, alpha_bonferroni)
p-значение:  0.08
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [38]:
statistic_check(dataA1, dataB, dataA1PSS, dataBPSS, alpha_bonferroni)
p-значение:  0.22
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными

Вывод:

во всех четырех группах по событиям была отвергнута нулевая гипотеза, значит у нас нет оснований, считать, что между группами А1 и В есть значимая разница.

Проведение сравнения между группами А2 и В¶

In [39]:
statistic_check(dataA2, dataB, dataA2MSA, dataBMSA, alpha_bonferroni)
p-значение:  0.52
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [40]:
statistic_check(dataA2, dataB, dataA2OSA, dataBOSA, alpha_bonferroni)
p-значение:  0.93
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [41]:
statistic_check(dataA2, dataB, dataA2CSA, dataBCSA, alpha_bonferroni)
p-значение:  0.59
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [42]:
statistic_check(dataA2, dataB, dataA2PSS, dataBPSS, alpha_bonferroni)
p-значение:  0.73
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными

Вывод:

во всех четырех группах по событиям была отвергнута нулевая гипотеза, значит у нас нет оснований, считать, что между группами А2 и В есть значимая разница.

Проведение сравнения между группами А и В¶

In [43]:
statistic_check(dataA, dataB, dataAMSA, dataBMSA, alpha_bonferroni)
p-значение:  0.35
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [44]:
statistic_check(dataA, dataB, dataAOSA, dataBOSA, alpha_bonferroni)
p-значение:  0.45
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [45]:
statistic_check(dataA, dataB, dataACSA, dataBCSA, alpha_bonferroni)
p-значение:  0.19
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
In [46]:
statistic_check(dataA, dataB, dataAPSS, dataBPSS, alpha_bonferroni)
p-значение:  0.61
Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными

Вывод:

во всех четырех группах по событиям была отвергнута нулевая гипотеза, значит у нас нет оснований, считать, что между обощенной группой А и группой В есть значимая разница.

Выводы и рекомендации¶

  1. Для проведения анализа результатов А/А/В тестирования:

    • были очищены и приведены к необходимым типам данные;
    • определен реальный исследуемый интервал (01.08.2019-07.08.2019);
    • определено, что событие Tutorial может быть исключено из рассмотрения в воронке событий;
    • при этом после очистки мы потеряли 1.16% событий и 0.28% пользователей, что не должно повлиять на дальнейший анализ.
  2. Предполагается, что воронка событий выглядит как: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful, при этом больше всего пользователей теряется на 1ом шаге при переходе с главного экрана на страницу с предложениями: остается 61.91% пользователей

  3. Определено, что между группами А отсутствует значимая разница, значит эксперимент можно считать достоверным.
  4. Между группой В и группой А1, группой А2, обобщенной группой А проверенных по событиям также нет оснований считать, что существует значимая разница, то есть изменение шрифта в приложении не повлияло на конверсию пользователей из одного шага в другой.

Рекомендации:

  • т.к. больше всего пользователей теряется на первом шаге, возможно, можно сделать изменение не во всем приложении, а только на странице предложений, например, более привлекательные стартовые картинки/баннеры, удобный фильтр, рекламные предложения и т.п.;
  • также стоит убедиться, что на первом шаге нет проблем для пользователя (не нажимается баннер, не работ переход, искажен тот или иной текст);
  • возможно и такое, что некоторые пользователи переходят со стартовой страницы на карточку товара с какого-нибудь рекламного баннера на стартовой странице;
  • если изменение шрифта не является затратным изменением, то от него можно не отказываться, но ожидать изменений бизнес-показателей не стоит.